﻿//  Copyright © 2009-2010 by Rhy A. Mednick
//  All rights reserved.
//  http://rhyduino.codeplex.com
//  
//  Redistribution and use in source and binary forms, with or without modification, 
//  are permitted provided that the following conditions are met:
//  
//  * Redistributions of source code must retain the above copyright notice, this list 
//    of conditions and the following disclaimer.
//  
//  * Redistributions in binary form must reproduce the above copyright notice, this 
//    list of conditions and the following disclaimer in the documentation and/or other 
//    materials provided with the distribution.
//  
//  * Neither the name of Rhy A. Mednick nor the names of its contributors may be used 
//    to endorse or promote products derived from this software without specific prior 
//    written permission.
//  
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
//  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
//  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
//  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
//  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
//  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
//  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using System;
using TracerX;

namespace Rhyduino
{
    /// <summary>
    /// Class used to represent and control the state of a digital pin on an Arduino.
    /// </summary>
    /// <remarks>
    /// This class can only be instantiated by another object in the Rhyduino library; you cannot create an
    /// instance of it on your own, and you cannot create derived classes from it.
    /// </remarks>
    public sealed class DigitalPin
    {
        #region Propertie(s)

        ///<summary>
        ///  Gets the mode of the pin.
        ///</summary>
        public PinMode Mode
        {
            get { return _mode; }
        }

        ///<summary>
        ///  The digital pin number.
        ///</summary>
        public int PinNumber
        {
            get { return _pinNumber; }
        }

        #endregion

        #region Constructor(s)

        ///<summary>
        ///  Constructs an instance of the class.
        ///</summary>
        ///<param name = "pinNumber">The number of the pin represented by the class.</param>
        ///<param name = "arduino">The related Arduino.</param>
        internal DigitalPin(Arduino arduino, int pinNumber)
        {
            if (2 > pinNumber || pinNumber > 13)
            {
                throw new InvalidOperationException("Index must be a value from 2-13.");
            }

            _pinNumber = pinNumber;
            _arduino = arduino;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Gets the value of a digital pin in INPUT or OUTPUT mode. The Arduino platform does not provide 
        /// a method for reading values of pins that are in an output mode. Therefore, when the pin mode 
        /// is OUTPUT the value returned is the last value that had been written to the pin.
        /// </summary>
        /// <param name="digitalPinValue">The digital pin value.</param>
        /// <returns>False if no value has been written to the pin (i.e. the internal cache is empty).</returns>
        public bool GetPinValue(out DigitalPinValue digitalPinValue)
        {
            var result = false;

            if (Mode == PinMode.Input || Mode == PinMode.Output)
            {
                digitalPinValue = _value;
                result = true;
            }
            else
            {
                digitalPinValue = DigitalPinValue.None;
            }

            return result;
        }

        /// <summary>
        /// Gets the value of a pin in SERVO mode. The Arduino platform does not provide a method for reading values of 
        /// pins that are in an output mode, and SERVO is one of those modes. Therefore, the value returned is the last 
        /// value that had been written to the pin.
        /// </summary>
        /// <param name="servoValue">The servo value.</param>
        /// <returns>False if no value has been written to the pin (i.e. the internal cache is empty).</returns>
        public bool GetPinValue(out short servoValue)
        {
            var result = false;

            servoValue = _servoValue;

            if (Mode == PinMode.Servo && servoValue != -1)
            {
                // Set flag indicating the out parameter is set to a valid value. 
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Gets the value of a pin in PWM mode. The Arduino platform does not provide a method for reading values of 
        /// pins that are in an output mode, and PWM is one of those modes. Therefore, the value returned is the last 
        /// value that had been written to the pin.
        /// </summary>
        /// <param name="pwmValue">The PWM value.</param>
        /// <returns>False if no value has been written to the pin (i.e. the internal cache is empty).</returns>
        public bool GetPinValue(out int pwmValue)
        {
            var result = false;

            pwmValue = _pwmValue;

            if (Mode == PinMode.Pwm && pwmValue != -1)
            {
                // Set flag indicating the out parameter is set to a valid value. 
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Sets the pin mode. When setting the pin to INPUT mode, the library automatically activates monitoring 
        /// for the associated port (Firmata doesn't allow monitoring of individual pins, instead it groups pins into 
        /// a "port").
        /// </summary>
        /// <param name="pinMode">The pin mode.</param>
        public void SetPinMode(PinMode pinMode)
        {
            _mode = pinMode;
            _arduino.Post(FirmataEncoder.BuildSetPinModeRequest(PinNumber, pinMode));
            if (pinMode == PinMode.Input)
            {
                // set the pull-down resistor so that the digital pin isn't floating.
                _arduino.Post(FirmataEncoder.BuildDigitalWriteRequest(PinToPort(PinNumber),
                                                                      _arduino.GetPortValue(PinNumber)));

                // Monitor the port containing the pin.
                _arduino.Post(FirmataEncoder.BuildReportDigitalPortRequest(PinToPort(PinNumber), true));
            }
        }

        /// <summary>
        /// Sets the value of a digital pin in INPUT or OUTPUT mode.
        /// </summary>
        /// <param name="digitalPinValue">The digital pin value.</param>
        /// <remarks>When the pin is in INPUT mode, this changes the value of the internal structure as if the value had 
        /// been changed because of a notification from the device. When in OUTPUT mode, this changes the value of the 
        /// internal structure and sends the change request to the attached device.</remarks>
        public void SetPinValue(DigitalPinValue digitalPinValue)
        {
            using (_log.DebugCall())
            {
                if ((Mode != PinMode.Input) && (Mode != PinMode.Output))
                {
                    throw new InvalidOperationException(
                        "A digital value can only be assigned to a pin in Input or Output mode.");
                }

                // Leave method if value is the same as what's stored.
                if (digitalPinValue == _value) return;

                // We have a pin value change
                if (Mode == PinMode.Output)
                {
                    _log.DebugFormat(
                        "Digital Pin [{0}] in output mode. Sending value change request to attached device. New value: {1}.",
                        PinNumber, digitalPinValue);
                    // If it's an output pin, send the information to the device.
                    _arduino.Post(FirmataEncoder.BuildDigitalWriteRequest(PinToPort(PinNumber),
                                                                          _arduino.GetPortValue(PinNumber)));
                }
                // Save the new value
                _log.DebugFormat("Digital Pin [{0}] changed from {1} to {2}.", PinNumber, _value, digitalPinValue);
                _value = digitalPinValue;
                // Notify observers of the value change.
                _arduino.OnDigitalPinValueChange(PinNumber, _value);
            }
        }

        /// <summary>
        /// Sets the value of a digital pin in PWM (pulse width modulation) mode.
        /// </summary>
        /// <param name="pwmValue">The PWM value.</param>
        public void SetPinValue(int pwmValue)
        {
            if (Mode != PinMode.Pwm)
            {
                throw new InvalidOperationException(
                    "A pulse width modulated value can only be written to a pin that is in PWM mode.");
            }

            _pwmValue = pwmValue;
            _arduino.Post(FirmataEncoder.BuildAnalogWriteRequest(PinNumber, pwmValue));
        }

        /// <summary>
        /// Sets the value of a pin in servo mode, effectively moving the servo to the specified position.
        /// </summary>
        /// <param name="servoValue">The servo position value.</param>
        public void SetPinValue(short servoValue)
        {
            if (Mode != PinMode.Servo)
            {
                throw new InvalidOperationException(
                    "A servo value can only be written to a pin that is in servo mode.");
            }

            _servoValue = servoValue;
            _arduino.Post(FirmataEncoder.BuildAnalogWriteRequest(PinNumber, servoValue));
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Returns the port number for the specified pin.
        /// </summary>
        /// <param name="pinNumber">The pin number.</param>
        /// <returns>The port number.</returns>
        private static int PinToPort(int pinNumber)
        {
            return (pinNumber <= 7) ? 0 : 1;
        }

        #endregion

        #region Private Fields

        private static readonly Logger _log = Logger.GetLogger("DigitalPin");

        private readonly Arduino _arduino;
        private readonly int _pinNumber;
        private PinMode _mode = PinMode.None;
        private int _pwmValue = -1;
        private short _servoValue = -1;
        private DigitalPinValue _value = DigitalPinValue.None;

        #endregion
    }
}